package WebPAC::Input;

use warnings;
use strict;

use blib;

use WebPAC::Common;
use base qw/WebPAC::Common/;
use Data::Dump qw/dump/;
use Encode qw/from_to/;

=head1 NAME

WebPAC::Input - read different file formats into WebPAC

=head1 VERSION

Version 0.16

=cut

our $VERSION = '0.16';

=head1 SYNOPSIS

This module implements input as database which have fixed and known
I<size> while indexing and single unique numeric identifier for database
position ranging from 1 to I<size>.

Simply, something that is indexed by unmber from 1 .. I<size>.

Examples of such databases are CDS/ISIS files, MARC files, lines in
text file, and so on.

Specific file formats are implemented using low-level interface modules,
located in C<WebPAC::Input::*> namespace which export C<open_db>,
C<fetch_rec> and optional C<init> functions.

Perhaps a little code snippet.

	use WebPAC::Input;

	my $db = WebPAC::Input->new(
		module => 'WebPAC::Input::ISIS',
	);

	$db->open( path => '/path/to/database' );
	print "database size: ",$db->size,"\n";
	while (my $rec = $db->fetch) {
		# do something with $rec
	}



=head1 FUNCTIONS

=head2 new

Create new input database object.

  my $db = new WebPAC::Input(
	module => 'WebPAC::Input::MARC',
	encoding => 'ISO-8859-2',
	recode => 'char pairs',
	no_progress_bar => 1,
  );

C<module> is low-level file format module. See L<WebPAC::Input::ISIS> and
L<WebPAC::Input::MARC>.

Optional parametar C<encoding> specify application code page (which will be
used internally). This should probably be your terminal encoding, and by
default, it C<ISO-8859-2>.

C<recode> is optional string constisting of character or words pairs that
should be replaced in input stream.

C<no_progress_bar> disables progress bar output on C<STDOUT>

This function will also call low-level C<init> if it exists with same
parametars.

=cut

sub new {
	my $class = shift;
	my $self = {@_};
	bless($self, $class);

	my $log = $self->_get_logger;

	$log->logconfess("code_page argument is not suppored any more. change it to encoding") if ($self->{lookup});
	$log->logconfess("lookup argument is not suppored any more. rewrite call to lookup_ref") if ($self->{lookup});
	$log->logconfess("low_mem argument is not suppored any more. rewrite it to load_row and save_row") if ($self->{low_mem});

	$log->logconfess("specify low-level file format module") unless ($self->{module});
	my $module_path = $self->{module};
	$module_path =~ s#::#/#g;
	$module_path .= '.pm';
	$log->debug("require low-level module $self->{module} from $module_path");

	require $module_path;

	$self->{'encoding'} ||= 'ISO-8859-2';

	$self ? return $self : return undef;
}

=head2 open

This function will read whole database in memory and produce lookups.

 my $store;	# simple in-memory hash

 $input->open(
 	path => '/path/to/database/file',
	code_page => 'cp852',
	limit => 500,
	offset => 6000,
	stats => 1,
	lookup_coderef => sub {
		my $rec = shift;
		# store lookups
	},
	modify_records => {
		900 => { '^a' => { ' : ' => '^b' } },
		901 => { '*' => { '^b' => ' ; ' } },
	},
	modify_file => 'conf/modify/mapping.map',
	save_row => sub {
		my $a = shift;
		$store->{ $a->{id} } = $a->{row};
	},
	load_row => sub {
		my $a = shift;
		return defined($store->{ $a->{id} }) &&
			$store->{ $a->{id} };
	},

 );

By default, C<code_page> is assumed to be C<cp852>.

C<offset> is optional parametar to position at some offset before reading from database.

C<limit> is optional parametar to read just C<limit> records from database

C<stats> create optional report about usage of fields and subfields

C<lookup_coderef> is closure to called to save data into lookups

C<modify_records> specify mapping from subfields to delimiters or from
delimiters to subfields, as well as oprations on fields (if subfield is
defined as C<*>.

C<modify_file> is alternative for C<modify_records> above which preserves order and offers
(hopefully) simplier sintax than YAML or perl (see L</modify_file_regex>). This option
overrides C<modify_records> if both exists for same input.

C<save_row> and C<load_row> are low-level implementation of store engine. Calling convention
is documented in example above.

Returns size of database, regardless of C<offset> and C<limit>
parametars, see also C<size>.

=cut

sub open {
	my $self = shift;
	my $arg = {@_};

	my $log = $self->_get_logger();

	$log->logconfess("lookup argument is not suppored any more. rewrite call to lookup_coderef") if ($arg->{lookup});
	$log->logconfess("lookup_coderef must be CODE, not ",ref($arg->{lookup_coderef}))
		if ($arg->{lookup_coderef} && ref($arg->{lookup_coderef}) ne 'CODE');

	$log->debug( $arg->{lookup_coderef} ? '' : 'not ', "using lookup_coderef");

	$log->logcroak("need path") if (! $arg->{'path'});
	my $code_page = $arg->{'code_page'} || 'cp852';

	# store data in object
	$self->{'input_code_page'} = $code_page;
	foreach my $v (qw/path offset limit/) {
		$self->{$v} = $arg->{$v} if ($arg->{$v});
	}

	if ($arg->{load_row} || $arg->{save_row}) {
		$log->logconfess("save_row and load_row must be defined in pair and be CODE") unless (
			ref($arg->{load_row}) eq 'CODE' &&
			ref($arg->{save_row}) eq 'CODE'
		);
		$self->{load_row} = $arg->{load_row};
		$self->{save_row} = $arg->{save_row};
		$log->debug("using load_row and save_row instead of in-memory hash");
	}

	my $filter_ref;
	my $recode_regex;
	my $recode_map;

	if ($self->{recode}) {
		my @r = split(/\s/, $self->{recode});
		if ($#r % 2 != 1) {
			$log->logwarn("recode needs even number of elements (some number of valid pairs)");
		} else {
			while (@r) {
				my $from = shift @r;
				my $to = shift @r;
				$recode_map->{$from} = $to;
			}

			$recode_regex = join '|' => keys %{ $recode_map };

			$log->debug("using recode regex: $recode_regex");
		}

	}

	my $rec_regex;
	if (my $p = $arg->{modify_file}) {
		$log->debug("using modify_file $p");
		$rec_regex = $self->modify_file_regexps( $p );
	} elsif (my $h = $arg->{modify_records}) {
		$log->debug("using modify_records ", sub { dump( $h ) });
		$rec_regex = $self->modify_record_regexps(%{ $h });
	}
	$log->debug("rec_regex: ", sub { dump($rec_regex) }) if ($rec_regex);

	my $class = $self->{module} || $log->logconfess("can't get low-level module name!");

	my $ll_db = $class->new(
		path => $arg->{path},
#		filter => sub {
#			my ($l,$f_nr) = @_;
#			return unless defined($l);
#			from_to($l, $code_page, $self->{'encoding'});
#			$l =~ s/($recode_regex)/$recode_map->{$1}/g if ($recode_regex && $recode_map);
#			return $l;
#		},
		%{ $arg },
	);

	unless (defined($ll_db)) {
		$log->logwarn("can't open database $arg->{path}, skipping...");
		return;
	}

	my $size = $ll_db->size;

	unless ($size) {
		$log->logwarn("no records in database $arg->{path}, skipping...");
		return;
	}

	my $from_rec = 1;
	my $to_rec = $size;

	if (my $s = $self->{offset}) {
		$log->debug("skipping to MFN $s");
		$from_rec = $s;
	} else {
		$self->{offset} = $from_rec;
	}

	if ($self->{limit}) {
		$log->debug("limiting to ",$self->{limit}," records");
		$to_rec = $from_rec + $self->{limit} - 1;
		$to_rec = $size if ($to_rec > $size);
	}

	# store size for later
	$self->{size} = ($to_rec - $from_rec) ? ($to_rec - $from_rec + 1) : 0;

	$log->info("processing $self->{size}/$size records [$from_rec-$to_rec] convert $code_page -> $self->{encoding}", $self->{stats} ? ' [stats]' : '');

	# read database
	for (my $pos = $from_rec; $pos <= $to_rec; $pos++) {

		$log->debug("position: $pos\n");

		my $rec = $ll_db->fetch_rec($pos, sub {
				my ($l,$f_nr) = @_;
#				return unless defined($l);
#				return $l unless ($rec_regex && $f_nr);

				$log->debug("-=> $f_nr ## $l");

				# codepage conversion and recode_regex
				from_to($l, $code_page, $self->{'encoding'});
				$l =~ s/($recode_regex)/$recode_map->{$1}/g if ($recode_regex && $recode_map);

				# apply regexps
				if ($rec_regex && defined($rec_regex->{$f_nr})) {
					$log->logconfess("regexps->{$f_nr} must be ARRAY") if (ref($rec_regex->{$f_nr}) ne 'ARRAY');
					my $c = 0;
					foreach my $r (@{ $rec_regex->{$f_nr} }) {
						my $old_l = $l;
						eval '$l =~ ' . $r;
						if ($old_l ne $l) {
							$log->debug("REGEX on $f_nr eval \$l =~ $r\n## old l: [$old_l]\n## new l: [$l]");
						}
						$log->error("error applying regex: $r") if ($@);
					}
				}

				$log->debug("<=- $f_nr ## $l");
				return $l;
		});

		$log->debug(sub { dump($rec) });

		if (! $rec) {
			$log->warn("record $pos empty? skipping...");
			next;
		}

		# store
		if ($self->{save_row}) {
			$self->{save_row}->({
				id => $pos,
				row => $rec,
			});
		} else {
			$self->{data}->{$pos} = $rec;
		}

		# create lookup
		$arg->{'lookup_coderef'}->( $rec ) if ($rec && $arg->{'lookup_coderef'});

		# update counters for statistics
		if ($self->{stats}) {

			# fetch clean record with regexpes applied for statistics
			my $rec = $ll_db->fetch_rec($pos);

			foreach my $fld (keys %{ $rec }) {
				$self->{_stats}->{fld}->{ $fld }++;

				$log->logdie("invalid record fild $fld, not ARRAY")
					unless (ref($rec->{ $fld }) eq 'ARRAY');
	
				foreach my $row (@{ $rec->{$fld} }) {

					if (ref($row) eq 'HASH') {

						foreach my $sf (keys %{ $row }) {
							next if ($sf eq 'subfields');
							$self->{_stats}->{sf}->{ $fld }->{ $sf }->{count}++;
							$self->{_stats}->{sf}->{ $fld }->{ $sf }->{repeatable}++
									if (ref($row->{$sf}) eq 'ARRAY');
						}

					} else {
						$self->{_stats}->{repeatable}->{ $fld }++;
					}
				}
			}
		}

		$self->progress_bar($pos,$to_rec) unless ($self->{no_progress_bar});

	}

	$self->{pos} = -1;
	$self->{last_pcnt} = 0;

	# store max mfn and return it.
	$self->{max_pos} = $to_rec;
	$log->debug("max_pos: $to_rec");

	# save for dump
	$self->{ll_db} = $ll_db;

	return $size;
}

=head2 fetch

Fetch next record from database. It will also displays progress bar.

 my $rec = $isis->fetch;

Record from this function should probably go to C<data_structure> for
normalisation.

=cut

sub fetch {
	my $self = shift;

	my $log = $self->_get_logger();

	$log->logconfess("it seems that you didn't load database!") unless ($self->{pos});

	if ($self->{pos} == -1) {
		$self->{pos} = $self->{offset};
	} else {
		$self->{pos}++;
	}

	my $mfn = $self->{pos};

	if ($mfn > $self->{max_pos}) {
		$self->{pos} = $self->{max_pos};
		$log->debug("at EOF");
		return;
	}

	$self->progress_bar($mfn,$self->{max_pos}) unless ($self->{no_progress_bar});

	my $rec;

	if ($self->{load_row}) {
		$rec = $self->{load_row}->({ id => $mfn });
	} else {
		$rec = $self->{data}->{$mfn};
	}

	$rec ||= 0E0;
}

=head2 pos

Returns current record number (MFN).

 print $isis->pos;

First record in database has position 1.

=cut

sub pos {
	my $self = shift;
	return $self->{pos};
}


=head2 size

Returns number of records in database

 print $isis->size;

Result from this function can be used to loop through all records

 foreach my $mfn ( 1 ... $isis->size ) { ... }

because it takes into account C<offset> and C<limit>.

=cut

sub size {
	my $self = shift;
	return $self->{size};
}

=head2 seek

Seek to specified MFN in file.

 $isis->seek(42);

First record in database has position 1.

=cut

sub seek {
	my $self = shift;
	my $pos = shift || return;

	my $log = $self->_get_logger();

	if ($pos < 1) {
		$log->warn("seek before first record");
		$pos = 1;
	} elsif ($pos > $self->{max_pos}) {
		$log->warn("seek beyond last record");
		$pos = $self->{max_pos};
	}

	return $self->{pos} = (($pos - 1) || -1);
}

=head2 stats

Dump statistics about field and subfield usage

  print $input->stats;

=cut

sub stats {
	my $self = shift;

	my $log = $self->_get_logger();

	my $s = $self->{_stats};
	if (! $s) {
		$log->warn("called stats, but there is no statistics collected");
		return;
	}

	my $max_fld = 0;

	my $out = join("\n",
		map {
			my $f = $_ || die "no field";
			my $v = $s->{fld}->{$f} || die "no s->{fld}->{$f}";
			$max_fld = $v if ($v > $max_fld);

			my $o = sprintf("%4s %d ~", $f, $v);

			if (defined($s->{sf}->{$f})) {
				map {
					$o .= sprintf(" %s:%d%s", $_, 
						$s->{sf}->{$f}->{$_}->{count},
						$s->{sf}->{$f}->{$_}->{repeatable} ? '*' : '',
					);
				} sort keys %{ $s->{sf}->{$f} };
			}

			if (my $v_r = $s->{repeatable}->{$f}) {
				$o .= " ($v_r)" if ($v_r != $v);
			}

			$o;
		} sort { $a cmp $b } keys %{ $s->{fld} }
	);

	$log->debug( sub { dump($s) } );

	return $out;
}

=head2 dump_ascii

Display humanly readable dump of record

=cut

sub dump_ascii {
	my $self = shift;

	return unless $self->{ll_db};

	if ($self->{ll_db}->can('dump_rec')) {
		return $self->{ll_db}->dump_ascii( $self->{pos} );
	} else {
		return dump( $self->{ll_db}->fetch_rec( $self->{pos} ) );
	}
}

=head2 modify_record_regexps

Generate hash with regexpes to be applied using l<filter>.

  my $regexpes = $input->modify_record_regexps(
		900 => { '^a' => { ' : ' => '^b' } },
		901 => { '*' => { '^b' => ' ; ' } },
  );

=cut

sub _get_regex {
	my ($sf,$from,$to) = @_;
	if ($sf =~ /^\^/) {
		return
			's/\Q'. $sf .'\E([^\^]*?)\Q'. $from .'\E([^\^]*?)/'. $sf .'$1'. $to .'$2/';
	} else {
		return
			's/\Q'. $from .'\E/'. $to .'/g';
	}
}

sub modify_record_regexps {
	my $self = shift;
	my $modify_record = {@_};

	my $regexpes;

	my $log = $self->_get_logger();

	foreach my $f (keys %$modify_record) {
		$log->debug("field: $f");

		foreach my $sf (keys %{ $modify_record->{$f} }) {
			$log->debug("subfield: $sf");

			foreach my $from (keys %{ $modify_record->{$f}->{$sf} }) {
				my $to = $modify_record->{$f}->{$sf}->{$from};
				#die "no field?" unless defined($to);
				$log->debug("transform: |$from| -> |$to|");

				my $regex = _get_regex($sf,$from,$to);
				push @{ $regexpes->{$f} }, $regex;
				$log->debug("regex: $regex");
			}
		}
	}

	return $regexpes;
}

=head2 modify_file_regexps

Generate hash with regexpes to be applied using l<filter> from
pseudo hash/yaml format for regex mappings.

It should be obvious:

	200
	  '^a'
	    ' : ' => '^e'
	    ' = ' => '^d'

In field I<200> find C<'^a'> and then C<' : '>, and replace it with C<'^e'>.
In field I<200> find C<'^a'> and then C<' = '>, and replace it with C<'^d'>.

  my $regexpes = $input->modify_file_regexps( 'conf/modify/common.pl' );

On undef path it will just return.

=cut

sub modify_file_regexps {
	my $self = shift;

	my $modify_path = shift || return;

	my $log = $self->_get_logger();

	my $regexpes;

	CORE::open(my $fh, $modify_path) || $log->logdie("can't open modify file $modify_path: $!");

	my ($f,$sf);

	while(<$fh>) {
		chomp;
		next if (/^#/ || /^\s*$/);

		if (/^\s*(\d+)\s*$/) {
			$f = $1;
			$log->debug("field: $f");
			next;
		} elsif (/^\s*'([^']*)'\s*$/) {
			$sf = $1;
			$log->die("can't define subfiled before field in: $_") unless ($f);
			$log->debug("subfield: $sf");
		} elsif (/^\s*'([^']*)'\s*=>\s*'([^']*)'\s*$/) {
			my ($from,$to) = ($1, $2);

			$log->debug("transform: |$from| -> |$to|");

			my $regex = _get_regex($sf,$from,$to);
			push @{ $regexpes->{$f} }, $regex;
			$log->debug("regex: $regex");
		}
	}

	return $regexpes;
}

=head1 AUTHOR

Dobrica Pavlinusic, C<< <dpavlin@rot13.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2005-2006 Dobrica Pavlinusic, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1; # End of WebPAC::Input
